// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE

(function(mod) {
  if (typeof exports == 'object' && typeof module == 'object')
    // CommonJS
    mod(require('../../lib/codemirror'));
  else if (typeof define == 'function' && define.amd)
    // AMD
    define(['../../lib/codemirror'], mod);
  // Plain browser env
  else mod(CodeMirror);
})(function(CodeMirror) {
  'use strict';

  CodeMirror.defineMode('julia', function(config, parserConf) {
    function wordRegexp(words, end) {
      if (typeof end === 'undefined') {
        end = '\\b';
      }
      return new RegExp('^((' + words.join(')|(') + '))' + end);
    }

    var octChar = '\\\\[0-7]{1,3}';
    var hexChar = '\\\\x[A-Fa-f0-9]{1,2}';
    var sChar = '\\\\[abefnrtv0%?\'"\\\\]';
    var uChar =
      '([^\\u0027\\u005C\\uD800-\\uDFFF]|[\\uD800-\\uDFFF][\\uDC00-\\uDFFF])';

    var operators =
      parserConf.operators ||
      wordRegexp(
        [
          '[<>]:',
          '[<>=]=',
          '<<=?',
          '>>>?=?',
          '=>',
          '->',
          '\\/\\/',
          '[\\\\%*+\\-<>!=\\/^|&\\u00F7\\u22BB]=?',
          '\\?',
          '\\$',
          '~',
          ':',
          '\\u00D7',
          '\\u2208',
          '\\u2209',
          '\\u220B',
          '\\u220C',
          '\\u2218',
          '\\u221A',
          '\\u221B',
          '\\u2229',
          '\\u222A',
          '\\u2260',
          '\\u2264',
          '\\u2265',
          '\\u2286',
          '\\u2288',
          '\\u228A',
          '\\u22C5',
          '\\b(in|isa)\\b(?!.?\\()',
        ],
        '',
      );
    var delimiters = parserConf.delimiters || /^[;,()[\]{}]/;
    var identifiers =
      parserConf.identifiers ||
      /^[_A-Za-z\u00A1-\u2217\u2219-\uFFFF][\w\u00A1-\u2217\u2219-\uFFFF]*!*/;

    var chars = wordRegexp([octChar, hexChar, sChar, uChar], "'");
    var openers = wordRegexp([
      'begin',
      'function',
      'type',
      'struct',
      'immutable',
      'let',
      'macro',
      'for',
      'while',
      'quote',
      'if',
      'else',
      'elseif',
      'try',
      'finally',
      'catch',
      'do',
    ]);
    var closers = wordRegexp(['end', 'else', 'elseif', 'catch', 'finally']);
    var keywords = wordRegexp([
      'if',
      'else',
      'elseif',
      'while',
      'for',
      'begin',
      'let',
      'end',
      'do',
      'try',
      'catch',
      'finally',
      'return',
      'break',
      'continue',
      'global',
      'local',
      'const',
      'export',
      'import',
      'importall',
      'using',
      'function',
      'where',
      'macro',
      'module',
      'baremodule',
      'struct',
      'type',
      'mutable',
      'immutable',
      'quote',
      'typealias',
      'abstract',
      'primitive',
      'bitstype',
    ]);
    var builtins = wordRegexp(['true', 'false', 'nothing', 'NaN', 'Inf']);

    var macro = /^@[_A-Za-z][\w]*/;
    var symbol = /^:[_A-Za-z\u00A1-\uFFFF][\w\u00A1-\uFFFF]*!*/;
    var stringPrefixes = /^(`|([_A-Za-z\u00A1-\uFFFF]*"("")?))/;

    function inArray(state) {
      return inGenerator(state, '[');
    }

    function inGenerator(state, bracket) {
      var curr = currentScope(state),
        prev = currentScope(state, 1);
      if (typeof bracket === 'undefined') {
        bracket = '(';
      }
      if (curr === bracket || (prev === bracket && curr === 'for')) {
        return true;
      }
      return false;
    }

    function currentScope(state, n) {
      if (typeof n === 'undefined') {
        n = 0;
      }
      if (state.scopes.length <= n) {
        return null;
      }
      return state.scopes[state.scopes.length - (n + 1)];
    }

    // tokenizers
    function tokenBase(stream, state) {
      // Handle multiline comments
      if (stream.match(/^#=/, false)) {
        state.tokenize = tokenComment;
        return state.tokenize(stream, state);
      }

      // Handle scope changes
      var leavingExpr = state.leavingExpr;
      if (stream.sol()) {
        leavingExpr = false;
      }
      state.leavingExpr = false;

      if (leavingExpr) {
        if (stream.match(/^'+/)) {
          return 'operator';
        }
      }

      if (stream.match(/\.{4,}/)) {
        return 'error';
      } else if (stream.match(/\.{1,3}/)) {
        return 'operator';
      }

      if (stream.eatSpace()) {
        return null;
      }

      var ch = stream.peek();

      // Handle single line comments
      if (ch === '#') {
        stream.skipToEnd();
        return 'comment';
      }

      if (ch === '[') {
        state.scopes.push('[');
      }

      if (ch === '(') {
        state.scopes.push('(');
      }

      var scope = currentScope(state);

      if (inArray(state) && ch === ']') {
        if (scope === 'for') {
          state.scopes.pop();
        }
        state.scopes.pop();
        state.leavingExpr = true;
      }

      if (inGenerator(state) && ch === ')') {
        if (scope === 'for') {
          state.scopes.pop();
        }
        state.scopes.pop();
        state.leavingExpr = true;
      }

      if (inArray(state)) {
        if (state.lastToken == 'end' && stream.match(/^:/)) {
          return 'operator';
        }
        if (stream.match(/^end/)) {
          return 'number';
        }
      }

      var match;
      if ((match = stream.match(openers, false))) {
        state.scopes.push(match[0]);
      }

      if (stream.match(closers, false)) {
        state.scopes.pop();
      }

      // Handle type annotations
      if (stream.match(/^::(?![:\$])/)) {
        state.tokenize = tokenAnnotation;
        return state.tokenize(stream, state);
      }

      // Handle symbols
      if (
        (!leavingExpr && stream.match(symbol)) ||
        stream.match(
          /:([<>]:|<<=?|>>>?=?|->|\/\/|\.{2,3}|[\.\\%*+\-<>!\/^|&]=?|[~\?\$])/,
        )
      ) {
        return 'builtin';
      }

      // Handle parametric types
      //if (stream.match(/^{[^}]*}(?=\()/)) {
      //  return "builtin";
      //}

      // Handle operators and Delimiters
      if (stream.match(operators)) {
        return 'operator';
      }

      // Handle Number Literals
      if (stream.match(/^\.?\d/, false)) {
        var imMatcher = RegExp(/^im\b/);
        var numberLiteral = false;
        // Floats
        if (stream.match(/^\d*\.(?!\.)\d*([Eef][\+\-]?\d+)?/i)) {
          numberLiteral = true;
        }
        if (stream.match(/^\d+\.(?!\.)\d*/)) {
          numberLiteral = true;
        }
        if (stream.match(/^\.\d+/)) {
          numberLiteral = true;
        }
        if (stream.match(/^0x\.[0-9a-f]+p[\+\-]?\d+/i)) {
          numberLiteral = true;
        }
        // Integers
        if (stream.match(/^0x[0-9a-f]+/i)) {
          numberLiteral = true;
        } // Hex
        if (stream.match(/^0b[01]+/i)) {
          numberLiteral = true;
        } // Binary
        if (stream.match(/^0o[0-7]+/i)) {
          numberLiteral = true;
        } // Octal
        if (stream.match(/^[1-9]\d*(e[\+\-]?\d+)?/)) {
          numberLiteral = true;
        } // Decimal
        // Zero by itself with no other piece of number.
        if (stream.match(/^0(?![\dx])/i)) {
          numberLiteral = true;
        }
        if (numberLiteral) {
          // Integer literals may be "long"
          stream.match(imMatcher);
          state.leavingExpr = true;
          return 'number';
        }
      }

      // Handle Chars
      if (stream.match(/^'/)) {
        state.tokenize = tokenChar;
        return state.tokenize(stream, state);
      }

      // Handle Strings
      if (stream.match(stringPrefixes)) {
        state.tokenize = tokenStringFactory(stream.current());
        return state.tokenize(stream, state);
      }

      if (stream.match(macro)) {
        return 'meta';
      }

      if (stream.match(delimiters)) {
        return null;
      }

      if (stream.match(keywords)) {
        return 'keyword';
      }

      if (stream.match(builtins)) {
        return 'builtin';
      }

      var isDefinition =
        state.isDefinition ||
        state.lastToken == 'function' ||
        state.lastToken == 'macro' ||
        state.lastToken == 'type' ||
        state.lastToken == 'struct' ||
        state.lastToken == 'immutable';

      if (stream.match(identifiers)) {
        if (isDefinition) {
          if (stream.peek() === '.') {
            state.isDefinition = true;
            return 'variable';
          }
          state.isDefinition = false;
          return 'def';
        }
        if (stream.match(/^({[^}]*})*\(/, false)) {
          state.tokenize = tokenCallOrDef;
          return state.tokenize(stream, state);
        }
        state.leavingExpr = true;
        return 'variable';
      }

      // Handle non-detected items
      stream.next();
      return 'error';
    }

    function tokenCallOrDef(stream, state) {
      var match = stream.match(/^(\(\s*)/);
      if (match) {
        if (state.firstParenPos < 0) state.firstParenPos = state.scopes.length;
        state.scopes.push('(');
        state.charsAdvanced += match[1].length;
      }
      if (currentScope(state) == '(' && stream.match(/^\)/)) {
        state.scopes.pop();
        state.charsAdvanced += 1;
        if (state.scopes.length <= state.firstParenPos) {
          var isDefinition = stream.match(
            /^(\s*where\s+[^\s=]+)*\s*?=(?!=)/,
            false,
          );
          stream.backUp(state.charsAdvanced);
          state.firstParenPos = -1;
          state.charsAdvanced = 0;
          state.tokenize = tokenBase;
          if (isDefinition) return 'def';
          return 'builtin';
        }
      }
      // Unfortunately javascript does not support multiline strings, so we have
      // to undo anything done upto here if a function call or definition splits
      // over two or more lines.
      if (stream.match(/^$/g, false)) {
        stream.backUp(state.charsAdvanced);
        while (state.scopes.length > state.firstParenPos) state.scopes.pop();
        state.firstParenPos = -1;
        state.charsAdvanced = 0;
        state.tokenize = tokenBase;
        return 'builtin';
      }
      state.charsAdvanced += stream.match(/^([^()]*)/)[1].length;
      return state.tokenize(stream, state);
    }

    function tokenAnnotation(stream, state) {
      stream.match(/.*?(?=,|;|{|}|\(|\)|=|$|\s)/);
      if (stream.match(/^{/)) {
        state.nestedLevels++;
      } else if (stream.match(/^}/)) {
        state.nestedLevels--;
      }
      if (state.nestedLevels > 0) {
        stream.match(/.*?(?={|})/) || stream.next();
      } else if (state.nestedLevels == 0) {
        state.tokenize = tokenBase;
      }
      return 'builtin';
    }

    function tokenComment(stream, state) {
      if (stream.match(/^#=/)) {
        state.nestedLevels++;
      }
      if (!stream.match(/.*?(?=(#=|=#))/)) {
        stream.skipToEnd();
      }
      if (stream.match(/^=#/)) {
        state.nestedLevels--;
        if (state.nestedLevels == 0) state.tokenize = tokenBase;
      }
      return 'comment';
    }

    function tokenChar(stream, state) {
      var isChar = false,
        match;
      if (stream.match(chars)) {
        isChar = true;
      } else if ((match = stream.match(/\\u([a-f0-9]{1,4})(?=')/i))) {
        var value = parseInt(match[1], 16);
        if (value <= 55295 || value >= 57344) {
          // (U+0,U+D7FF), (U+E000,U+FFFF)
          isChar = true;
          stream.next();
        }
      } else if ((match = stream.match(/\\U([A-Fa-f0-9]{5,8})(?=')/))) {
        var value = parseInt(match[1], 16);
        if (value <= 1114111) {
          // U+10FFFF
          isChar = true;
          stream.next();
        }
      }
      if (isChar) {
        state.leavingExpr = true;
        state.tokenize = tokenBase;
        return 'string';
      }
      if (!stream.match(/^[^']+(?=')/)) {
        stream.skipToEnd();
      }
      if (stream.match(/^'/)) {
        state.tokenize = tokenBase;
      }
      return 'error';
    }

    function tokenStringFactory(delimiter) {
      if (delimiter.substr(-3) === '"""') {
        delimiter = '"""';
      } else if (delimiter.substr(-1) === '"') {
        delimiter = '"';
      }
      function tokenString(stream, state) {
        if (stream.eat('\\')) {
          stream.next();
        } else if (stream.match(delimiter)) {
          state.tokenize = tokenBase;
          state.leavingExpr = true;
          return 'string';
        } else {
          stream.eat(/[`"]/);
        }
        stream.eatWhile(/[^\\`"]/);
        return 'string';
      }
      return tokenString;
    }

    var external = {
      startState: function() {
        return {
          tokenize: tokenBase,
          scopes: [],
          lastToken: null,
          leavingExpr: false,
          isDefinition: false,
          nestedLevels: 0,
          charsAdvanced: 0,
          firstParenPos: -1,
        };
      },

      token: function(stream, state) {
        var style = state.tokenize(stream, state);
        var current = stream.current();

        if (current && style) {
          state.lastToken = current;
        }

        return style;
      },

      indent: function(state, textAfter) {
        var delta = 0;
        if (
          textAfter === ']' ||
          textAfter === ')' ||
          textAfter === 'end' ||
          textAfter === 'else' ||
          textAfter === 'catch' ||
          textAfter === 'elseif' ||
          textAfter === 'finally'
        ) {
          delta = -1;
        }
        return (state.scopes.length + delta) * config.indentUnit;
      },

      electricInput: /\b(end|else|catch|finally)\b/,
      blockCommentStart: '#=',
      blockCommentEnd: '=#',
      lineComment: '#',
      fold: 'indent',
    };
    return external;
  });

  CodeMirror.defineMIME('text/x-julia', 'julia');
});
